/**
 * Copyright (c) 2009 Blue Whale Systems Ltd
 * 
 * This software is provided "as is," and the copyright holder makes no representations or warranties, express or
 * implied, including but not limited to warranties of merchantability or fitness for any particular purpose or that the
 * use of this software or documentation will not infringe any third party patents, copyrights, trademarks or other
 * rights.
 * 
 * The copyright holder will not be liable for any direct, indirect special or consequential damages arising out of any
 * use of this software or documentation.
 */
package com.sun.lwuit;

import com.bluewhalesystems.client.lwuit.richtext.RichTextAreaModel;
import com.sun.lwuit.geom.Dimension;
import com.sun.lwuit.plaf.*;

/**
 * Displays rich text, where 'rich' refers to variations in font size and style.
 */
public class RichTextArea extends TextArea
{
    public static final char INSTRUCTION_DRAW_TEXT      = 0;
    public static final char INSTRUCTION_DRAW_TEXT_LINE = 1;
    public static final char INSTRUCTION_NEWLINE        = 2;
    public static final char INSTRUCTION_NEWLINE_WITH_X = 3;
    public static final char INSTRUCTION_MOVE_XY        = 4;
    public static final char INSTRUCTION_SET_FONT       = 5;
    public static final char INSTRUCTION_SET_COLOR      = 6;
    public static final char INSTRUCTION_DRAW_OBJECT    = 7;
    public static final char INSTRUCTION_SET_ROW_GAP    = 8;

    public Font getDefaultFont()
    {
        return getStyle().getFont();
    }

    public Font getFont( char aFaceSizeAndStyleValue )
    {
        int face = aFaceSizeAndStyleValue & 0x60;
        int size = aFaceSizeAndStyleValue & 0x18;
        int style = aFaceSizeAndStyleValue & 0x7;
        return Font.createSystemFont( face, style, size );
    }

    public void setModel( RichTextAreaModel aModel )
    {
        iModel = aModel;
    }

    public RichTextAreaModel getModel()
    {
        return iModel;
    }

    public int getHeightEstimate( int aWidth )
    {
        return iContentHeight;
    }

    // Component

    public void paint( Graphics aGraphics )
    {
        if( null == iModel )
        {
            UIManager.getInstance().getLookAndFeel().drawRichTextArea( aGraphics, this, iChars, iLines, iLinesCount );
        }
        else
        {
            UIManager.getInstance().getLookAndFeel().drawRichTextArea2( aGraphics, this );
        }
    }

    protected Dimension calcPreferredSize()
    {
        return UIManager.getInstance().getLookAndFeel().getRichTextAreaPreferredSize( this );
    }

    protected Dimension calcScrollSize()
    {
        return UIManager.getInstance().getLookAndFeel().getRichTextAreaPreferredSize( this );
    }

    // TextArea

    public int getLines()
    {
        return iLinesCount;
    }

    protected void initRowString()
    {
        widthForRowCalculations = getWidth();
        iLinesCount = 0;
        iContentHeight = 0;
        iChars = new char[] {};

        String text = getText();

        if( 0 >= widthForRowCalculations || null == text || 0 == text.length() )
        {
            return;
        }

        char[] chars = text.toCharArray();

        Style style = getStyle();
        Font font = style.getFont();

        // 'widthForRowCalculations' is not actually the value used for calculations. It is a flag to indicate that 
        // a width change has occurred. The actual width used for the purposes of fitting individual lines of text 
        // needs to take into account the padding on either side and the possibility of a scrollbar being drawn.
        int width = widthForRowCalculations;
        width -= style.getPadding( RIGHT ) + style.getPadding( LEFT );

        // Set the width used to determine when enough characters are included when brute-dividing a substring to be
        // twice the width of the widest character. This may be a little too much and it may be possible to be a little
        // more precise in the future.
        iBruteDivideThreshold = font.charWidth( getWidestChar() ) * 2;

        if( isSingleLineTextArea() )
        {
            if( font.charsWidth( chars, 0, chars.length ) < width )
            {
                replaceUnprintableCharacters( chars, 0, chars.length );
                addLine( chars, 0, chars.length );
            }
            else
            {
                // Find a shorter substring that fits in a single row and, for cosmetic purposes, put '...' at the end.
                int delta = getBruteCountForSingleRow( chars, 0, chars.length, width, font );

                if( MINIMUM_LENGTH_BEFORE_INSERTING_ELLIPSIS < delta )
                {
                    if( false )
                    {
                        // This usually does not work. The built-in fonts do not have extended ascii support it seems.
                        chars[delta - 1] = ASCII_ELLIPSIS;
                    }
                    else
                    {
                        chars[delta - 1] = '.';
                        chars[delta - 2] = '.';
                        chars[delta - 3] = '.';
                    }
                }

                replaceUnprintableCharacters( chars, 0, delta );
                addLine( chars, 0, delta );
            }
        }
        else
        {
            textToRows( chars, 0, width, font );
        }

        iChars = chars;
        iContentHeight = ( getUnselectedStyle().getFont().getHeight() + getRowsGap() ) * iLinesCount;
    }

    /**
     * Divides the text content into a list of substrings, each of which is no wider than the provided width.
     * 
     * @param aChars the input character array
     * @param aOffset beginning offset of the substring to consider
     * @param aWidth width available to draw the substring
     * @param aFont font used to draw the substring
     */
    protected void textToRows( char[] aChars, int aOffset, int aWidth, Font aFont )
    {
        int delta = 0;
        int length = aChars.length;
        int lastSoftBreakOffset = -1;

        try
        {
            while( aOffset + delta < length )
            {
                switch( aChars[aOffset + delta] )
                {
                    case ASCII_CR:
                    case ASCII_TAB:
                        aChars[aOffset + delta] = ASCII_SPACE;
                        // Fall-through intentionally...

                    case ASCII_SPACE:
                    case '.':
                    case '-':
                    case '/':
                    case '\\':
                    case '_':
                    case '@':
                    case '?':
                    case '=':
                    case '&':
                        if( aFont.charsWidth( aChars, aOffset, delta ) < aWidth )
                        {
                            // Reset the 'soft' line break marker with a new value.
                            lastSoftBreakOffset = delta;
                        }
                        else
                        {
                            // We could wait till the next 'hard' line break (or indeed, EOF) but since we already know
                            // that the substring does not fit, trigger the code that normally occurs on 'hard' breaks.
                            if( -1 != lastSoftBreakOffset )
                            {
                                lastSoftBreakOffset++;
                                addLine( aChars, aOffset, lastSoftBreakOffset );
                                aOffset += lastSoftBreakOffset;
                                lastSoftBreakOffset = -1;
                                delta = 0;
                            }
                            else
                            {
                                bruteTextToRows( aChars, aOffset, delta, aWidth, aFont );
                                aOffset += delta;
                                delta = 0;
                            }
                        }

                        delta++;
                        break;

                    case ASCII_LF:
                        if( aFont.charsWidth( aChars, aOffset, delta ) < aWidth )
                        {
                            // The 'normal' situation, a 'hard' line break with all the accumulated content fitting
                            // nicely into a single row.
                            aChars[aOffset + delta] = ASCII_SPACE;
                            addLine( aChars, aOffset, delta );
                            aOffset += delta;
                        }
                        else
                        {
                            // Use the most recent 'soft' line break position or brute-divide the substring.
                            if( -1 != lastSoftBreakOffset )
                            {
                                lastSoftBreakOffset++;
                                addLine( aChars, aOffset, lastSoftBreakOffset );
                                aOffset += lastSoftBreakOffset;
                                lastSoftBreakOffset = -1;
                            }
                            else
                            {
                                aChars[aOffset + delta] = ASCII_SPACE;
                                bruteTextToRows( aChars, aOffset, delta, aWidth, aFont );
                                aOffset += delta;
                            }
                        }

                        delta = 0;
                        break;

                    default:
                        delta++;
                        break;
                }
            }

            if( aOffset < length )
            {
                // This logic runs pretty much every time because there's usually going to be some 'tail' substring. In 
                // the extreme case when neither a 'hard' nor a 'soft' line break is found, all the text content will be
                // processed here.
                if( aFont.charsWidth( aChars, aOffset, length - aOffset ) < aWidth )
                {
                    addLine( aChars, aOffset, length - aOffset );
                }
                else
                {
                    if( -1 != lastSoftBreakOffset )
                    {
                        lastSoftBreakOffset++;
                        addLine( aChars, aOffset, lastSoftBreakOffset );
                        textToRows( aChars, aOffset + lastSoftBreakOffset, aWidth, aFont );
                    }
                    else
                    {
                        bruteTextToRows( aChars, aOffset, length - aOffset, aWidth, aFont );
                    }
                }
            }
        }
        catch( RuntimeException re )
        {
            // Catch and print a little debugging info before re-throwing.
            // See ticket:3511 Unable to open viewer -- WrappingTextError problem with Facebook emails
            System.out.print( "WrappingTextArea.textToRows in back-recursion aOffset[" + aOffset + "] length[" + length + "], delta[" + delta + "] aChars:[" + new String( aChars ) + "]" );
            throw re;
        }
    }

    /**
     * Generates one or more rows out of a substring when neither a 'soft' nor a 'hard' line break is found.
     * 
     * Note: This method assumes that the provided substring does NOT fit fully in a single row.
     * 
     * @param aChars the input character array
     * @param aOffset beginning offset of the substring to consider
     * @param aCount length of the substring to consider
     * @param aWidth width available to draw the substring
     * @param aFont font used to draw the substring
     */
    protected void bruteTextToRows( char[] aChars, int aOffset, int aCount, int aWidth, Font aFont )
    {
        int count = getBruteCountForSingleRow( aChars, aOffset, aCount, aWidth, aFont );
        int offset = aOffset + count;
        int finalCount = aCount - count;

        addLine( aChars, aOffset, count );

        if( offset < aOffset + aCount )
        {
            if( aFont.charsWidth( aChars, offset, finalCount ) < aWidth )
            {
                addLine( aChars, offset, finalCount );
            }
            else
            {
                bruteTextToRows( aChars, offset, finalCount, aWidth, aFont );
            }
        }
    }

    /**
     * Locates the position of the last character to be shown on any particular line when neither a 'soft' 
     * nor a 'hard' line break is found.
     * 
     * @param aChars the input character array
     * @param aOffset beginning offset of the substring to consider
     * @param aCount length of the substring to consider
     * @param aWidth width available to draw the substring
     * @param aFont font used to draw the substring
     * @return the delta (position) of the last character in the substring that fits on a single row
     */
    protected int getBruteCountForSingleRow( char[] aChars, int aOffset, int aCount, int aWidth, Font aFont )
    {
        int count = 0;
        int width = -1;

        if( -1 == iGoodCharactersPerLineEstimate )
        {
            // We do not have a good estimate as to how many characters will fit so we start with a default value and
            // then hop around until that value can be calculated and cached for future reference.
            //
            // Also, see comment for DEFAULT_CHARS_PER_LINE_COUNT_ESTIMATE.
            count = DEFAULT_CHARS_PER_LINE_COUNT_ESTIMATE;
            width = aWidth - aFont.charsWidth( aChars, aOffset, count );

            if( 0 <= width )
            {
                if( iBruteDivideThreshold <= width )
                {
                    do
                    {
                        count += DEFAULT_CHARS_PER_LINE_COUNT_DELTA;

                        if( aChars.length < aOffset + count )
                        {
                            break;
                        }

                        width = aWidth - aFont.charsWidth( aChars, aOffset, count );
                    }
                    while( iBruteDivideThreshold <= width );

                    while( 0 > width )
                    {
                        count--;
                        width = aWidth - aFont.charsWidth( aChars, aOffset, count );
                    }
                }
            }
            else
            {
                do
                {
                    if( DEFAULT_CHARS_PER_LINE_COUNT_DELTA >= count )
                    {
                        break;
                    }

                    count -= DEFAULT_CHARS_PER_LINE_COUNT_DELTA;
                    width = aWidth - aFont.charsWidth( aChars, aOffset, count );
                }
                while( 0 >= width );

                while( 0 < width )
                {
                    count++;
                    width = aWidth - aFont.charsWidth( aChars, aOffset, count );
                }
            }

            iGoodCharactersPerLineEstimate = count;
        }
        else
        {
            count = iGoodCharactersPerLineEstimate;

            // Since we already have a 'good' delta estimate, just calculate that width and add/substract a character 
            // at a time depending on the result. This in most case will result in no more than one or two extra 
            // comparisons.

            if( 0 <= width )
            {
                while( iBruteDivideThreshold <= width )
                {
                    count++;
                    width = aWidth - aFont.charsWidth( aChars, aOffset, count );
                }
            }
            else
            {
                while( 0 > width )
                {
                    count--;
                    width = aWidth - aFont.charsWidth( aChars, aOffset, count );
                }
            }
        }

        return count;
    }

    /**
     * Replaces unprintable characters (such as tabs and carriage returns) with spaces.
     * 
     * Note: This implementation is used for (and is meant for) short strings. It is more efficient to use 
     * {@link String#replace(char, char)} multiple times when you're not sure how short the string is.
     * 
     * @param aChars the input character array
     * @param aOffset beginning offset of the substring to consider
     * @param aDelta length of the substring to consider
     */
    protected static void replaceUnprintableCharacters( char[] aChars, int aOffset, int aDelta )
    {
        for( int index = aOffset; index < aOffset + aDelta; ++index )
        {
            switch( aChars[index] )
            {
                case ASCII_TAB:
                case ASCII_CR:
                    // Replace these unprintable characters with spaces.
                    aChars[index] = ASCII_SPACE;
                    break;
            }
        }
    }

    protected void addLine( char[] aChars, int aOffset, int aLength )
    {
        if( null == iLines )
        {
            // Attempt to be a little smart about this. Most text areas we have are one or two lines tall (on most
            // phones). So our default initial array is 4 elements long (to allow for very narrow screens). However, 
            // we also take a look at how long the content array is and if it appears to be large, we jump ahead and
            // allocate a much larger array.

            if( aChars.length > 1000 )
            {
                // No idea how many lines it will actually be but lets pick a somewhat large number.
                iLines = new int[200];
            }
            else
            {
                // Pick smaller number realizing that most of our text areas are at most a couple of lines tall.
                iLines = new int[aChars.length > 80 ? 10 : 4];
            }

            //System.out.println( "WrappingTextArea.addLine new lines length[" + iLines.length + "] chars length[" + aChars.length + "]" );
        }
        else if( iLines.length - 1 == iLinesCount )
        {
            // We keep the increases linear for very small number of lines and for a large number of lines but
            // jump more aggressively from around 10 immediately to 100.
            int length = 100;

            if( iLines.length < 10 || iLinesCount >= 100 )
            {
                length = iLines.length * 2;
            }

            int[] newLines = new int[length];
            System.arraycopy( iLines, 0, newLines, 0, iLinesCount );

            // Explicitely assign null which apparently helps under some VMs.
            // SonyEricsson "Developers guidelines - November 2007 Java Platform, Micro Edition, CLDC - MIDP 2"
            iLines = null;
            iLines = newLines;

            //System.out.println( "WrappingTextArea.addLine resized lines length[" + iLines.length + "] chars length[" + aChars.length + "]" );
        }

        iLines[iLinesCount++] = aLength;
    }

    /**
     * For single row content that is wider than the component's width, the minimum length (in characters) required 
     * before an 'ellipsis' is inserted at the end.
     */
    private static final int    MINIMUM_LENGTH_BEFORE_INSERTING_ELLIPSIS = 10;

    /**
     * Named characters (for the sake of convenience).
     */
    private static final char   ASCII_CR                                 = 0x0D;
    private static final char   ASCII_LF                                 = 0x0A;
    private static final char   ASCII_TAB                                = 0x09;
    private static final char   ASCII_SPACE                              = 0x20;
    private static final char   ASCII_ELLIPSIS                           = 0x85;

    /**
     * A (very rough) guess at how many characters will fit on a full line. This is currently set in stone but an
     * alternative would be to calculate it like so:
     * 
     * 'aWidth / aFont.charWidth( getWidestChar() )'
     * 
     * However, that is not optimal either because it underestimates the number of character. For example, with my 
     * default setting in the WTK emulator, the calculated value is 23, whereas, on average, about 34 character
     * seem to fit in a real-world test case. It's not terribly important to get this spot-on because we minimize
     * how often the 'good' estimate is calculated.
     */
    private static final int    DEFAULT_CHARS_PER_LINE_COUNT_ESTIMATE    = 30;

    /**
     * Number of characters to 'hop' when attempting to calculate the best 'count' value.
     */
    private static final int    DEFAULT_CHARS_PER_LINE_COUNT_DELTA       = 10;

    /**
     * A valid estimate for how many characters can fit on any given line. The idea is to calculate this infrequently 
     * (currently done only once) and cache. It is a shared value for all text area components because in our setup 
     * all text components are the same width. 
     */
    protected static int        iGoodCharactersPerLineEstimate           = -1;

    /**
     * A width value (in pixels) used to determine when a sufficient number of characters will be displayed on a 
     * single line to constitute a 'full' line. In other words, if the remaining width is equal or less we call it
     * a day and move on to the next line. This value is only used when we are forced to brute-divide a substring
     * because neither a 'soft' nor a 'hard' line break is found.
     * 
     * This value should be somewhat larger than the width of the widest character. Currently we only support the
     * single font but in the future we'll need to use the largest of the fonts (with the 'bold' flag set) to
     * set this value.
     */
    protected int               iBruteDivideThreshold;

    /**
     * Raw character data.
     */
    protected char[]            iChars;

    /**
     * Per-line descriptor (offset, height, etc).
     */
    protected int[]             iLines;

    /**
     * Current number of lines.
     */
    protected int               iLinesCount;

    /**
     * Total height of all the content. Currently all lines have the same height so this is easily calculated. However,
     * eventually each line will have it's own height so it will be useful to cache this value along the way.
     */
    protected int               iContentHeight;

    /**
     * A representation of the data to draw. Some of it persistent, some of it not.
     */
    protected RichTextAreaModel iModel;
}